import time
import struct

#import serial

CMD_END = '\r'
#CMD_END = '\n\r' # DEBUG

# System Commands/Parameters
CMD_RESET = 'MR'
CMD_ICD = 'W{n}' # n = 0 to 255.  ICD = n * (400 us)
ICD_DEFAULT = 255
ICD_TIMESTEP = 400e-6
CMD_STATUS = '?S'
[
    STATUS_BUSY,        STATUS_SRQ, 
    STATUS_TRIGGERED,   STATUS_SCAN_COMPLETE, 
    STATUS_TRIG_MISSED, STATUS_BAD_PARAM, 
    STATUS_AD_OVF,      STATUS_BAD_CMD
] = [1 << bit for bit in range(8)[::-1]]

# Scanning
CMD_EOR = 'Z{eor}' # eor = end of record characters to send
EOR_DEFAULT = '\xFF\xFF'
CMD_ASYNC = 'MA'
CMD_SYNC = 'MS'


CMD_TRIG_EVERY = 'T{n}' # n = 1 to 2**15-1
CMD_TRIG_MASK = 'DT'
CMD_TRIG_UNMASK = 'ET'

NUM_AIO = 8
MAX_AIO = +10.237
MIN_AIO = -10.237
CMD_AIO_CFG = 'I{n}' # n = 0 to 8
AIO_DEFAULT = 0
CMD_AIO_GET = '?{n}' # n = 1 to 8
CMD_AIO_SET = 'S{n}={x}' # n = 0 to 9, x = -10.237 to +10.237

CMD_DIO_GET = '?B{n}' # n = 1 or 2
CMD_DIO_SET = 'SB{n}={m}' # n = 1 or 2, m = 0, 1, or I

INPUT = 'I'

CMD_SCAN = 'SC{ik}:{n}' 
# ik = comma seperated list of inputs to scan on trigger, 1-8 and/or D
# n = number of scans 
# NOTE: n * ik (number of inputs) cannot exceed 3711.
CMD_SCAN_DUMP = 'X'

CMD_SCAN_STREAM = 'SS{ik}:{n}'
# ik = comma seperated list of inputs to scan on trigger, 1-8 and/or D
# n = number of scans
# NOTES:
#  - n * ik (number of inputs) cannot exceed 32766.
#  - data will be sent back as soon as possible , 2 bytes/sample
#  - buffer cannot hold more than 3711 samples, so beware a trigger 
#        running faster than the data bus can handle for too long

CMD_SCAN_END = 'ES' 

BUF_LIM = 7420
MSG_LIM = 2**16 - 1

MAX_RATE = 2100

class sr245(object):
    def __init__(self, port=None, outputs=0):
        self.port = port
        self.buf = ''
        self.reset()
        
        # configure
        self.outputs = outputs
        
    def send_command(self,cmd,**kwargs):
        self.port.write(cmd.format(**kwargs) + CMD_END)
        #print('sr245 command> ' + cmd.format(**kwargs) + CMD_END)
        
    def reset(self):
        self.send_command(CMD_RESET)
        # internal default states after reset
        self._eor = EOR_DEFAULT
        self._outputs = AIO_DEFAULT
        self._icd = ICD_DEFAULT
        
        self.mask_trigger()
        self.icd = 0
        
    def disconnect(self):
        self.port.close()
        self.port = None
        
    # Configure intercharacter delay (RS-232 only)
    def get_icd(self):
        return ICD_TIMESTEP * self._icd
    def set_icd(self, delay=0):
        n = int(delay // ICD_TIMESTEP + 0.5)
        assert 0 <= n <= 255
        if self._icd != n:
            self._icd = n
            self.send_command(CMD_ICD, n=n)
    icd = property(get_icd, set_icd)
        
    # Configure End-of-record to be transmitted at the end of a scan sequence
    def get_eor(self):
        return self._eor
    def set_eor(self, eor_new):
        self._eor = eor_new
        self.send_command(CMD_EOR, eor=eor_new)
    eor = property(get_eor, set_eor)
        
    # Trigger control
    def mask_trigger(self,mask=True):
        self.send_command(CMD_TRIG_MASK if mask else CMD_TRIG_UNMASK)
    def unmask_trigger(self): self.mask_trigger(mask=False)
    
    # Configure Analog I/O
    def get_outputs(self):
        return self._outputs
    def set_outputs(self, outputs_new):
        assert 0 <= outputs_new <= NUM_AIO
        if self._outputs != outputs_new: 
            self._outputs = outputs_new
            self.send_command(CMD_AIO_CFG, n=outputs_new)
    outputs = property(get_outputs, set_outputs)

    def sync_mode(self, sync=True):
        self.send_command(CMD_SYNC if sync else CMD_ASYNC)
    def async_mode(self): self.syncmode(sync=False)
    
    def scan_start(self, n, channels=[1], stream=False):
        assert 2 * n * len(channels) < (MSG_LIM if stream else BUF_LIM)
        self.send_command(
            CMD_SCAN_STREAM if stream else CMD_SCAN,
            ik=','.join(str(ch) for ch in channels),
            n=n)
    
    def scan(self, n, channels=[1], stream=False, timeout=0.05, cb_running=None):
        self.scan_start(n, channels=channels, stream=stream)
        
        buf = ''
        breaktimer = time.time()
        while True:
            # check if we have some minimum number
            if len(buf) > 4:
                if cb_running is not None:
                    cb_running()
                    cb_running = None
                # if we have some amount, look for a break
                if self.port.inWaiting():
                    # if new data, reset timer
                    breaktimer = time.time()
                else:
                    if time.time() - breaktimer > timeout:
                        # if no data received in the span of [timeout] s, stop scan
                        self.scan_end()
                        break
            else:
                # force timer reset until minimum data reached
                breaktimer = time.time()
                
            # read data            
            buf += self.port.read(self.port.inWaiting())
         
        return block_convert(buf)
        
        #    if self._eor in self.buf:
        #        result, _, self.buf = (
        #                self.buf[:self.buf.find(self.eor)], 
        #                self.buf[self.buf.find(self.eor):self.buf.find(self.eor)+len(self.eor)],
        #                self.buf[self.buf.find(self.eor)+len(self.eor):])
        #                
        #        return result
        #
        #self.scan_end()
        #
        #return self.buf
    
    def scan_end(self):
        self.send_command(CMD_SCAN_END)
    
    def write_d(self,pin,val):
        self.send_command(CMD_DIO_SET,n=pin,m=int(bool(val)))
		
	def get_status(self, timeout=0.05):
		self.send_command(CMD_STATUS)
		begin = time.time()
		while True:
			if not self.port.inWaiting():
				if time.time() - begin > timeout:
					return None
			else:
				pass
		
    '''
    def read(self, ch, num, sync):
        assert 0 <= ch < NUM_AIO
        self.send_command(CMD_AIO_GET, n=ch)
        
        
    def readscan(self):
        self.buf += self.port.read(self.port.inWaiting())
                
        if self._eor in self.buf:
            slice, _, self.buf = (self.buf[:self.buf.find(self._eor)], 
                                  self.buf[self.buf.find(self._eor):self.buf.find(self._eor)+2],
                                  self.buf[self.buf.find(self._eor)+2:])
        
            return slice
    
    def _test(self):
        # run through tests, return False if one fails, else fall-through and
        # give True.
        if 0:
            return False
        if 0:
            return False
        
        return True
    '''

def block_convert(buf):
    # drop converting last sample as it may(/should?) be EOR (0xFF,0xFF)
    raw = struct.unpack_from('>' + (len(buf)/2 - 1)*'h', buf)
    return [convert(n) for n in raw]
	
def convert(n):
	# SR245 returns data in some asinine format:
	# bit: 1111 11
	#      5432 1098 7654 3210
	#      000s dddd dddd dddd
	# s = sign, 0 = positive, 1 = negative
	# d = data bits
	if n & (1 << 12):
		n = -(n ^ (1 << 12))
	return n